# engine.py
import json
import os
from datetime import datetime, timezone, timedelta
from typing import Optional, Type, Dict, Any

from entity.account import Account
from entity.position import Position, PositionDirection
from entity.strategy_signal import StrategySignal, SignalType
from strategy.strategy import Strategy, MultiTimeframeStrategy  # 导入修改后的策略名
# 修改导入路径
from entity.k_lines import KLines, KLine

beijing_tz = timezone(timedelta(hours=8))
import matplotlib

try:
    # 尝试一个通用的、通常能工作的后端
    matplotlib.use('TkAgg')
    # 或者 'Qt5Agg', 'Agg' (非交互式，保存到文件) 等
    print("Matplotlib backend set to TkAgg.")
except ImportError:
    print("TkAgg backend for Matplotlib not available, trying default.")


class Operator:
    def __init__(
            self,
            strategy_class: Type[Strategy],
            execution_klines_data: KLines,  # 1分钟 KLines
            other_tf_klines_map: Optional[Dict[str, KLines]] = None,  # 例如 {"5m": klines_5m_obj}
            strategy_params: Optional[dict] = None,
            initial_balance: float = 10000.0,
            open_commission_rate: float = 0.0008,
            close_commission_rate: float = 0.001,
            slippage_percent: float = 0.0005,
            leverage: float = 1.0,
            position_save_dir: str = r"F:\personal\python-project\backtest-v1\backtest_file\positions"
    ):
        if not execution_klines_data or not execution_klines_data.all_klines:
            raise ValueError("主执行周期的 KLines 数据不能为空。")

        self.execution_klines_data: KLines = execution_klines_data
        self.symbol: str = self.execution_klines_data.coin
        # --- 这是创建 all_klines_by_period 的地方 ---
        # 确保这里有 self.all_klines_by_period = ... 的赋值
        _all_klines_temp: Dict[str, KLines] = {}  # 使用一个临时变量
        exec_period_key = "1m"  # 默认或从 execution_klines_data.date_type 获取
        if hasattr(execution_klines_data, 'date_type') and execution_klines_data.date_type:
            exec_period_key = execution_klines_data.date_type
        else:
            print(f"警告: execution_klines_data 对象缺少有效的 'date_type' 属性，默认为 '{exec_period_key}' 作为键。")

        _all_klines_temp[exec_period_key] = execution_klines_data

        if other_tf_klines_map:  # 注意这里的参数名是 other_tf_klines_map
            for period, klines_obj in other_tf_klines_map.items():
                if klines_obj.coin != self.symbol:
                    raise ValueError(
                        f"所有KLines对象的交易对必须相同。主执行周期: {self.symbol}, 周期 {period}: {klines_obj.coin}")
                _all_klines_temp[period] = klines_obj

        self.all_klines_by_period = _all_klines_temp  # << --- 关键的赋值！确保这行存在！
        # --- all_klines_by_period 创建结束 ---

        self.account: Account = Account(initial_balance=initial_balance)
        self.account: Account = Account(initial_balance=initial_balance)
        self.strategy_params_config: Optional[dict] = strategy_params  # 保存策略配置参数

        # --- 初始化指标计算器 ---
        self.indicator_calculators: Dict[str, IndicatorCalculator] = {}

        # 为主执行周期创建指标计算器 (如果策略需要1分钟指标)
        # exec_period_name = execution_klines_data.date_type if hasattr(execution_klines_data, 'date_type') else "1m"
        # self.indicator_calculators[exec_period_name] = IndicatorCalculator(execution_klines_data.all_klines)
        # self.indicator_calculators[exec_period_name].add_ema(length=10) # 示例：计算1m EMA(10)

        # 为其他时间周期创建并预计算指标
        if other_tf_klines_map:
            for period_str, klines_obj in other_tf_klines_map.items():
                if klines_obj.coin != self.symbol:
                    raise ValueError(f"交易对不一致: 主周期 {self.symbol}, 其他周期 {period_str} 为 {klines_obj.coin}")

                print(f"为周期 {period_str} 初始化指标计算器...")
                calc = IndicatorCalculator(klines_obj.all_klines)
                if period_str == "5m" and self.strategy_params_config:  # 根据策略参数计算指标
                    ema_len = self.strategy_params_config.get("ema_period_5m", 20)
                    rsi_len = self.strategy_params_config.get("rsi_period_5m", 8)
                    boll_len = self.strategy_params_config.get("boll_period_5m", 20)
                    boll_std = float(self.strategy_params_config.get("boll_nbdev_5m", 2.0))

                    calc.add_ema(length=ema_len)
                    calc.add_rsi(length=rsi_len)
                    calc.add_bollinger_bands(length=boll_len, std=boll_std)
                    print(f"  周期 {period_str}: 添加了 EMA({ema_len}), RSI({rsi_len}), BBANDS({boll_len},{boll_std})")

                self.indicator_calculators[period_str] = calc

        # 现在才实例化策略，因为它可能在__init__中就需要指标计算器的信息（虽然当前设计不需要）
        self.strategy: Strategy = strategy_class(**(self.strategy_params_config or {}))

        self.open_commission_rate: float = open_commission_rate
        # ... (构造函数其余部分与上个版本一致) ...
        self.close_commission_rate: float = close_commission_rate
        self.slippage_percent: float = slippage_percent
        self.leverage: float = leverage
        self.current_position: Optional[Position] = None
        Position.SAVE_DIR = position_save_dir
        Position._position_id_counter = 0
        self.trade_log: list = []
        self.current_take_profit_target: Optional[float] = None
        self.current_stop_loss_target: Optional[float] = None
        self.entry_price_for_tp_sl: Optional[float] = None

        print(f"回测操作器已为交易对 {self.symbol} 初始化。")
        main_exec_period_name = execution_klines_data.date_type if hasattr(execution_klines_data,
                                                                           'date_type') else "1m"  # 假设
        print(f"主执行周期: {main_exec_period_name}")
        if other_tf_klines_map: print(f"其他参考周期: {list(other_tf_klines_map.keys())}")
        print(f"策略: {self.strategy.get_name()}")
        print(f"初始资金: {self.account.initial_balance:.2f}")
        print(f"开仓手续费率: {self.open_commission_rate * 100:.3f}%")
        print(f"平仓手续费率: {self.close_commission_rate * 100:.3f}%")
        print(f"滑点百分比: {self.slippage_percent * 100:.3f}%")
        print(f"杠杆: {self.leverage}x")
        print(f"所有时间将以北京时间 (UTC+8) 显示和处理。")

    # _apply_slippage, _convert_utc_ms_to_beijing_datetime, _reset_tp_sl_targets,
    # _handle_signal, _check_price_based_tp_sl, save_report 方法与上个版本相同

    def _apply_slippage(self, price: float, operation_type: str) -> float:  # 完全不变
        if self.slippage_percent == 0: return price
        if operation_type == "buy":
            return price * (1 + self.slippage_percent)
        elif operation_type == "sell":
            return price * (1 - self.slippage_percent)
        return price

    def _convert_utc_ms_to_beijing_datetime(self, timestamp_ms: int) -> datetime:  # 完全不变
        utc_dt = datetime.fromtimestamp(timestamp_ms / 1000, tz=timezone.utc)
        return utc_dt.astimezone(beijing_tz)

    def _reset_tp_sl_targets(self):  # 完全不变
        self.current_take_profit_target = None
        self.current_stop_loss_target = None
        self.entry_price_for_tp_sl = None

    def _handle_signal(self, signal: StrategySignal, current_kline: KLine):  # 完全不变
        if not signal or signal.signal_type == SignalType.HOLD: return
        timestamp = signal.timestamp
        intended_price = signal.price if signal.price is not None else current_kline.close
        size = signal.size
        log_entry_base = {
            "timestamp": timestamp.isoformat(),
            "kline_close_time": self._convert_utc_ms_to_beijing_datetime(current_kline.close_time).isoformat(),
            "signal_type": signal.signal_type.name, "price_from_signal": signal.price,
            "intended_execution_price": intended_price, "size": size, "comment": signal.comment,
            "action_taken": "",
            "position_id_before": self.current_position.position_id if self.current_position else None,
            "position_size_before": self.current_position.current_size if self.current_position and self.current_position.is_open else 0,
        }
        print(
            f"\n处理信号: {signal} 基于 K线收盘于 {self._convert_utc_ms_to_beijing_datetime(current_kline.close_time).strftime('%Y-%m-%d %H:%M:%S %Z%z')}")
        if (self.current_position is None or not self.current_position.is_open) and \
                signal.signal_type in [SignalType.OPEN_LONG, SignalType.OPEN_SHORT]:
            self.current_position = Position(
                symbol=self.symbol, account_ref=self.account,
                open_commission_rate=self.open_commission_rate,
                close_commission_rate=self.close_commission_rate,
                leverage=self.leverage
            )
        elif not (self.current_position and self.current_position.is_open) and \
                signal.signal_type not in [SignalType.OPEN_LONG, SignalType.OPEN_SHORT, SignalType.HOLD]:
            log_entry = {**log_entry_base, "action_taken": "忽略信号 (无活动仓位且非开仓信号)"}
            self.trade_log.append(log_entry)
            return
        if self.current_position is None and signal.signal_type not in [SignalType.OPEN_LONG, SignalType.OPEN_SHORT]:
            log_entry = {**log_entry_base, "action_taken": "忽略信号 (无仓位对象处理)"}
            self.trade_log.append(log_entry)
            return
        execution_price = intended_price
        action_taken_details = ""
        op_type_for_slippage = ""
        if signal.signal_type in [SignalType.OPEN_LONG, SignalType.ADD_TO_LONG]:
            op_type_for_slippage = "buy"
        elif signal.signal_type == SignalType.CLOSE_SHORT:
            op_type_for_slippage = "buy"
        elif signal.signal_type in [SignalType.OPEN_SHORT, SignalType.ADD_TO_SHORT]:
            op_type_for_slippage = "sell"
        elif signal.signal_type == SignalType.CLOSE_LONG:
            op_type_for_slippage = "sell"
        elif signal.signal_type == SignalType.EXIT_ALL and self.current_position and self.current_position.is_open:
            op_type_for_slippage = "sell" if self.current_position.direction == PositionDirection.LONG else "buy"
        if op_type_for_slippage: execution_price = self._apply_slippage(intended_price, op_type_for_slippage)
        log_entry = {**log_entry_base, "actual_execution_price": execution_price}
        if signal.signal_type == SignalType.OPEN_LONG:
            if self.current_position.is_open and self.current_position.direction == PositionDirection.SHORT:
                price_for_closing_short = self._apply_slippage(intended_price, "buy")
                closed = self.current_position.close(price_for_closing_short, timestamp,
                                                     reason="策略信号反转平仓 (空->多)")
                if closed: self._reset_tp_sl_targets()
            if not self.current_position.is_open:
                if size is None or size <= 0: size = 1
                opened = self.current_position.open(PositionDirection.LONG, execution_price, size, timestamp)
                if opened:
                    self.current_take_profit_target = signal.take_profit_price
                    self.current_stop_loss_target = signal.stop_loss_price
                    self.entry_price_for_tp_sl = execution_price
                    print(
                        f"  开多仓成功 @ {execution_price:.4f}. TP: {self.current_take_profit_target}, SL: {self.current_stop_loss_target}")
                action_taken_details = f"尝试开多仓 {'成功' if opened else '失败'}"
            else:
                action_taken_details = "忽略开多信号 (已有持仓)"
        elif signal.signal_type == SignalType.OPEN_SHORT:
            if self.current_position.is_open and self.current_position.direction == PositionDirection.LONG:
                price_for_closing_long = self._apply_slippage(intended_price, "sell")
                closed = self.current_position.close(price_for_closing_long, timestamp,
                                                     reason="策略信号反转平仓 (多->空)")
                if closed: self._reset_tp_sl_targets()
            if not self.current_position.is_open:
                if size is None or size <= 0: size = 1
                opened = self.current_position.open(PositionDirection.SHORT, execution_price, size, timestamp)
                if opened:
                    self.current_take_profit_target = signal.take_profit_price
                    self.current_stop_loss_target = signal.stop_loss_price
                    self.entry_price_for_tp_sl = execution_price
                    print(
                        f"  开空仓成功 @ {execution_price:.4f}. TP: {self.current_take_profit_target}, SL: {self.current_stop_loss_target}")
                action_taken_details = f"尝试开空仓 {'成功' if opened else '失败'}"
            else:
                action_taken_details = "忽略开空信号 (已有持仓)"
        elif signal.signal_type in [SignalType.CLOSE_LONG, SignalType.CLOSE_SHORT, SignalType.EXIT_ALL]:
            closed_successfully = False
            if self.current_position and self.current_position.is_open:
                if (
                        signal.signal_type == SignalType.CLOSE_LONG and self.current_position.direction == PositionDirection.LONG) or \
                        (
                                signal.signal_type == SignalType.CLOSE_SHORT and self.current_position.direction == PositionDirection.SHORT) or \
                        (signal.signal_type == SignalType.EXIT_ALL):
                    if size is None or size >= self.current_position.current_size:
                        closed_successfully = self.current_position.close(execution_price, timestamp,
                                                                          reason=f"策略信号: {signal.signal_type.name}")
                    elif size > 0:
                        closed_successfully = self.current_position.adjust(-size, execution_price, timestamp)
                action_taken_details = f"尝试平仓 (类型: {signal.signal_type.name}) @ {execution_price:.4f} {'成功' if closed_successfully else '失败或不适用'}"
                if closed_successfully and (
                        not self.current_position.is_open or self.current_position.current_size == 0):
                    self._reset_tp_sl_targets()
            else:
                action_taken_details = f"忽略平仓信号 {signal.signal_type.name} (无对应持仓或已关闭)"
        elif signal.signal_type == SignalType.ADD_TO_LONG:
            if self.current_position and self.current_position.is_open and self.current_position.direction == PositionDirection.LONG:
                if size is None or size <= 0: size = self.current_position.initial_size * 0.1
                adjusted = self.current_position.adjust(size, execution_price, timestamp, is_opening_adjustment=True)
                action_taken_details = f"尝试多仓加仓 {size:.4f} @ {execution_price:.4f} {'成功' if adjusted else '失败'}"
            else:
                action_taken_details = "忽略多仓加仓"
        log_entry["action_taken"] = action_taken_details
        log_entry["position_id_after"] = self.current_position.position_id if self.current_position else None
        log_entry[
            "position_size_after"] = self.current_position.current_size if self.current_position and self.current_position.is_open else 0
        log_entry[
            "position_direction_after"] = self.current_position.direction.name if self.current_position and self.current_position.is_open else "NONE"
        self.trade_log.append(log_entry)

    def _check_price_based_tp_sl(self, kline_bar: KLine):  # 完全不变
        if not (self.current_position and self.current_position.is_open): return
        high_price = kline_bar.high
        low_price = kline_bar.low
        close_time_dt = self._convert_utc_ms_to_beijing_datetime(kline_bar.close_time)
        if self.current_position.direction == PositionDirection.LONG:
            if self.current_stop_loss_target is not None and low_price <= self.current_stop_loss_target:
                execution_sl_price = self._apply_slippage(self.current_stop_loss_target, "sell")
                print(
                    f"  多单止损触发! K线L {low_price:.4f} <= SL目标 {self.current_stop_loss_target:.4f}. 执行价(滑): {execution_sl_price:.4f}")
                self.current_position.close(execution_sl_price, close_time_dt, reason="多单价格触碰止损")
                self._reset_tp_sl_targets()
                return
            if self.current_take_profit_target is not None and high_price >= self.current_take_profit_target:
                execution_tp_price = self._apply_slippage(self.current_take_profit_target, "sell")
                print(
                    f"  多单止盈触发! K线H {high_price:.4f} >= TP目标 {self.current_take_profit_target:.4f}. 执行价(滑): {execution_tp_price:.4f}")
                self.current_position.close(execution_tp_price, close_time_dt, reason="多单价格触碰止盈")
                self._reset_tp_sl_targets()
                return
        elif self.current_position.direction == PositionDirection.SHORT:
            if self.current_stop_loss_target is not None and high_price >= self.current_stop_loss_target:
                execution_sl_price = self._apply_slippage(self.current_stop_loss_target, "buy")
                print(
                    f"  空单止损触发! K线H {high_price:.4f} >= SL目标 {self.current_stop_loss_target:.4f}. 执行价(滑): {execution_sl_price:.4f}")
                self.current_position.close(execution_sl_price, close_time_dt, reason="空单价格触碰止损")
                self._reset_tp_sl_targets()
                return
            if self.current_take_profit_target is not None and low_price <= self.current_take_profit_target:
                execution_tp_price = self._apply_slippage(self.current_take_profit_target, "buy")
                print(
                    f"  空单止盈触发! K线L {low_price:.4f} <= TP目标 {self.current_take_profit_target:.4f}. 执行价(滑): {execution_tp_price:.4f}")
                self.current_position.close(execution_tp_price, close_time_dt, reason="空单价格触碰止盈")
                self._reset_tp_sl_targets()
                return

    def run_backtest(self):
        main_exec_period_name = self.execution_klines_data.date_type if hasattr(self.execution_klines_data,
                                                                                'date_type') else "1m"
        print(f"\n--- 开始回测: {self.symbol} (主执行周期: {main_exec_period_name}) ---")
        # ... (打印数据范围和账户重置不变)
        available_periods_info = {}
        for period_str, klines_obj in self.all_klines_by_period.items():  # 迭代原始KLines对象字典
            if klines_obj and klines_obj.all_klines:
                start_time_str = self._convert_utc_ms_to_beijing_datetime(klines_obj.all_klines[0].open_time).strftime(
                    '%Y-%m-%d %H:%M:%S %Z%z')
                end_time_str = self._convert_utc_ms_to_beijing_datetime(klines_obj.all_klines[-1].close_time).strftime(
                    '%Y-%m-%d %H:%M:%S %Z%z')
                available_periods_info[period_str] = f"{start_time_str} -> {end_time_str}"
            else:
                available_periods_info[period_str] = "无数据或KLines对象为空"

        print(f"可用数据周期范围 (北京时间): {available_periods_info}")

        self.account.reset()
        # ... (清理旧仓位和重置TP/SL目标不变)
        if self.current_position and self.current_position.is_open:
            cleanup_time_bj = self._convert_utc_ms_to_beijing_datetime(
                self.execution_klines_data.all_klines[0].open_time - 1000)
            self.current_position.close(self.execution_klines_data.all_klines[0].open, cleanup_time_bj,
                                        "回测开始前清理")
        self.current_position = None
        self._reset_tp_sl_targets()

        for i, execution_kline_bar in enumerate(self.execution_klines_data.all_klines):
            current_beijing_time = self._convert_utc_ms_to_beijing_datetime(execution_kline_bar.close_time)

            # 1. 更新市价和账户 (不变)
            unrealized_pnl_total = 0.0
            if self.current_position and self.current_position.is_open:
                self.current_position.update_market_price(execution_kline_bar.close, current_beijing_time)
                unrealized_pnl_total = self.current_position.unrealized_pnl
            self.account.update_open_positions_pnl(unrealized_pnl_total, current_beijing_time)

            # 2. 检查价格触发的止盈止损 (不变)
            if self.current_position and self.current_position.is_open:
                self._check_price_based_tp_sl(execution_kline_bar)

            # 3. 获取策略信号 (传递指标计算器)
            #    只有在没有被价格止盈止损平仓的情况下才获取新信号
            if not (self.current_position and self.current_position.is_open and \
                    (self.current_take_profit_target is not None or self.current_stop_loss_target is not None) and \
                    not hasattr(self.current_position, '_just_closed_by_tp_sl')):  # 避免在同一K线上重复开仓，如果需要更严格的控制

                # 清除可能存在的临时标记（如果使用的话）
                if hasattr(self.current_position, '_just_closed_by_tp_sl'):
                    delattr(self.current_position, '_just_closed_by_tp_sl')

                signal = self.strategy.generate_signal(
                    indicator_calculators=self.indicator_calculators,  # 传递指标计算器
                    current_execution_kline=execution_kline_bar,
                    execution_klines_obj=self.execution_klines_data,
                    current_position=self.current_position if self.current_position and self.current_position.is_open else None,
                    account=self.account
                )

                if signal:
                    self._handle_signal(signal, execution_kline_bar)

            if (i + 1) % 1000 == 0:  # 打印进度 (不变)
                print(
                    f"  回测进度: {i + 1}/{len(self.execution_klines_data.all_klines)} K线已处理. 当前权益: {self.account.equity:.2f}")

        # ... (回测结束后的强制平仓和报告生成不变) ...
        if self.current_position and self.current_position.is_open:
            print("\n回测结束，强制平掉剩余持仓...")
            last_kline = self.execution_klines_data.all_klines[-1]
            op_type_force_close = "sell" if self.current_position.direction == PositionDirection.LONG else "buy"
            force_close_price_slippage = self._apply_slippage(last_kline.close, op_type_force_close)
            force_close_beijing_time = self._convert_utc_ms_to_beijing_datetime(last_kline.close_time)
            self.current_position.close(force_close_price_slippage, force_close_beijing_time, reason="回测结束强制平仓")
            self.account.update_open_positions_pnl(0, force_close_beijing_time)
            self._reset_tp_sl_targets()

        print("--- 回测完成 ---")
        performance = self.account.get_performance_summary()
        print("\n--- 回测性能报告 ---")
        for key, value in performance.items():
            if key not in ["equity_curve", "trades_history"]:
                if isinstance(value, float):
                    print(f"{key}: {value:.2f}")
                else:
                    print(f"{key}: {value}")
        return self.account.get_performance_summary()

    def save_report(self, performance_summary: dict, report_dir: str = "backtest_reports"):  # 完全不变
        os.makedirs(report_dir, exist_ok=True)
        strategy_name_safe = "".join(c if c.isalnum() else "_" for c in self.strategy.get_name())
        filename = f"report_{self.symbol}_{strategy_name_safe}_{datetime.now(beijing_tz).strftime('%Y%m%d_%H%M%S')}.json"
        filepath = os.path.join(report_dir, filename)
        try:
            if 'equity_curve' in performance_summary and performance_summary['equity_curve']:
                new_equity_curve = []
                for entry in performance_summary['equity_curve']:
                    dt_val, equity_val = entry
                    if isinstance(dt_val, datetime):
                        new_equity_curve.append((dt_val.isoformat(), equity_val))
                    else:
                        new_equity_curve.append(entry)
                performance_summary['equity_curve'] = new_equity_curve
            if 'trades_history' in performance_summary and performance_summary['trades_history']:
                for trade_idx, trade in enumerate(performance_summary['trades_history']):
                    if isinstance(trade, dict):
                        for time_key in ['entry_time', 'close_time']:
                            if time_key in trade and isinstance(trade[time_key], datetime):
                                trade[time_key] = trade[time_key].isoformat()
                        if 'transaction_log' in trade and isinstance(trade['transaction_log'], list):
                            for tx_idx, tx in enumerate(trade['transaction_log']):
                                if isinstance(tx, dict) and 'timestamp' in tx and isinstance(tx['timestamp'], datetime):
                                    tx['timestamp'] = tx['timestamp'].isoformat()
                    else:
                        print(f"警告: trades_history[{trade_idx}] 不是字典: {trade}")
            with open(filepath, 'w', encoding='utf-8') as f:
                json.dump(performance_summary, f, indent=4, ensure_ascii=False)
            print(f"回测报告已保存至: {filepath}")
        except Exception as e:
            print(f"保存回测报告失败: {e}")
            import traceback
            traceback.print_exc()


# --- `if __name__ == '__main__':` (与上个版本类似, 但策略类名和参数可能需要调整) ---
# engine.py (或者一个新的 run_optimization.py)

# ... (所有必要的导入，包括新的类)

from utils.indicators import IndicatorCalculator
from optimization.optimizer import GridSearchOptimizer  # 导入优化器
from visualization.plotter import Plotter  # 导入绘图器

if __name__ == '__main__':
    print("加载K线数据...")
    try:
        klines_1m = KLines(
            coin="ETHUSDT", date_type="1m",
            start_date_str="2023-01", end_date_str="2023-01",  # 用少量数据测试优化
            base_data_path=r"F:\personal\binance_klines"
        )
        klines_5m = KLines(
            coin="ETHUSDT", date_type="5m",
            start_date_str="2023-01", end_date_str="2023-01",
            base_data_path=r"F:\personal\binance_klines"
        )
        if not klines_1m.all_klines or not klines_5m.all_klines:
            print("错误：未能加载1分钟或5分钟K线数据。")
            exit()

        other_klines_map = {"5m": klines_5m}
    except Exception as e:
        print(f"加载K线数据时出错: {e}")
        import traceback

        traceback.print_exc()
        exit()

    # --- 1. 定义参数优化空间 ---
    param_grid_for_optimization = {
        # MultiTimeframeStrategy 的参数
        "ema_period_5m": [180, 200, 220],
        "rsi_period_5m": [7, 8, 9],
        "boll_period_5m": [15, 20],
        "boll_nbdev_5m": [2.0],  # 固定一个值，或者也加入列表
        "rsi_lower_threshold": [82],
        "rsi_upper_threshold": [90],
        # 可以选择不优化止盈止损，或者也加入进来
        # "take_profit_pcent": [0.04, 0.046, 0.05],
        # "stop_loss_pcent": [0.003, 0.004, 0.005],
    }


    # --- 2. 定义评估指标函数 ---
    def evaluate_by_profit_drawdown_ratio(performance: Dict[str, Any]) -> float:
        profit = performance.get("total_net_profit", 0.0)
        max_drawdown = performance.get("max_drawdown_pcent", 100.0)  # 百分比
        if max_drawdown == 0 or max_drawdown >= 100:  # 避免除零或极端回撤
            return profit / 1000.0  # 给一个惩罚或小的基准值
        return profit / max_drawdown  # 目标：高利润，低回撤


    print("\n--- 开始参数优化 ---")
    optimizer = GridSearchOptimizer(
        operator_class=Operator,  # <--- 修改点: 传递 Operator 类
        execution_klines=klines_1m,
        other_tf_klines_map=other_klines_map,
        strategy_class=MultiTimeframeStrategy,  # 传入策略类
        param_grid_dict=param_grid_for_optimization,
        evaluation_metric=evaluate_by_profit_drawdown_ratio,  # 使用自定义评估函数
        # evaluation_metric=lambda p: p.get("profit_factor", 0.0), # 或者用盈利因子
        initial_balance=10000.0,  # 减少优化时的初始资金，加快速度
        open_commission_rate=0.0008,
        close_commission_rate=0.001,
        slippage_percent=0.0005,
    )

    best_params, best_performance = optimizer.run_optimization()

    if best_params and best_performance:
        print("\n--- 最优参数下的回测结果 ---")
        for key, value in best_performance.items():
            if key not in ["equity_curve", "trades_history"]:
                if isinstance(value, float):
                    print(f"{key}: {value:.2f}")
                else:
                    print(f"{key}: {value}")

        # --- 3. 可视化 ---
        results_df = optimizer.get_results_dataframe()
        # print("\n--- 优化结果DataFrame ---")
        # print(results_df.head())
        # results_df.to_csv("optimization_results.csv", index=False) # 保存结果到CSV

        plotter = Plotter(results_df)

        # 绘制最优参数下的权益曲线
        if best_performance.get("equity_curve"):
            plotter.plot_equity_curve(best_performance["equity_curve"], title=f"最优参数权益曲线 (参数: {best_params})")

        # 绘制参数平面图 (选择两个你最想看的参数作为x,y轴)
        # 例如，看 ema_period_5m 和 rsi_period_5m 对 评分(score) 的影响
        # 注意：param_x_name 和 param_y_name 必须与 param_grid_for_optimization 中的键完全一致
        # 并且 get_results_dataframe 中添加了 "param_" 前缀
        plotter.plot_parameter_plane(
            param_x_name="param_ema_period_5m",
            param_y_name="param_rsi_period_5m",
            metric_to_plot_name="score",  # 或者 "total_net_profit"
            title="参数平面: EMA vs RSI (颜色: Score)"
        )
        plotter.plot_3d_surface(  # 也可以尝试3D图
            param_x_name="param_ema_period_5m",
            param_y_name="param_rsi_period_5m",
            metric_to_plot_name="score",
            title="3D参数曲面: EMA vs RSI (Z轴: Score)"
        )

    else:
        print("参数优化未能找到有效结果。")
